﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using OpenTK;
using OpenTK.Graphics.OpenGL4;
using OpenTK.Input;
using System.Drawing;
using OpenTK.Graphics;
using Common;

namespace T18_Particles {
    class T18_ParticlesGameWindow : GameWindow {
        struct Particle :IComparable<Particle>{
            public Vector3 pos, speed;
            public byte r, g, b, a; // Color
            public float size, angle, weight;
            public float life; // Remaining life of the particle. if <0 : dead and unused.
            public float cameradistance; // *Squared* distance to the camera. if dead : -1.0f



            public int CompareTo(Particle other) {
                return other.cameradistance.CompareTo(this.cameradistance);
            }

            public static bool operator <(Particle thisParticle,Particle that) {
                // Sort in reverse order : far particles drawn first.
                return thisParticle.cameradistance > that.cameradistance;
            }
            public static bool operator >(Particle thisParticle, Particle that) {
                return thisParticle.cameradistance < that.cameradistance;
            }

        }
        public T18_ParticlesGameWindow() {
            this.Load += T18_ParticlesGameWindow_Load;
            this.Resize += T18_ParticlesGameWindow_Resize;
            this.RenderFrame += T18_ParticlesGameWindow_RenderFrame;
            this.Unload += T18_ParticlesGameWindow_Unload;
            this.Width = 1024;
            this.Height = 768;
            this.Title = "T18_ParticlesGameWindow";

        }

        
        private void T18_ParticlesGameWindow_RenderFrame(object sender, FrameEventArgs e) {
            GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

            Render();

            SwapBuffers();
            ProcessEvents();
        }


        const int MaxParticles = 100000;
        List<Particle> ParticlesContainer = new List<Particle>(MaxParticles);

        int LastUsedParticle = 0;

        // Finds a Particle in ParticlesContainer which isn't used yet.
        // (i.e. life < 0);
        int FindUnusedParticle() {

            for (int i = LastUsedParticle; i < MaxParticles; i++) {
                if (ParticlesContainer[i].life < 0) {
                    LastUsedParticle = i;
                    return i;
                }
            }

            for (int i = 0; i < LastUsedParticle; i++) {
                if (ParticlesContainer[i].life < 0) {
                    LastUsedParticle = i;
                    return i;
                }
            }

            return 0; // All particles are taken, override the first one
        }
        void SortParticles() {
            ParticlesContainer.Sort();
        }
        Vao vao;
        Shader shader;
        Texture2D texture;
        float[] g_vertex_buffer_data = {
         -0.5f, -0.5f, 0.0f,
          0.5f, -0.5f, 0.0f,
         -0.5f,  0.5f, 0.0f,
          0.5f,  0.5f, 0.0f,
    };
        float[] g_particule_position_size_data = new float[MaxParticles * 4];
        byte[] g_particule_color_data = new byte[MaxParticles * 4];
        Vbo<float> billboardVertexBuffer;
        Vbo<float> particlesPositionBuffer;
        Vbo<float> particlesColorBuffer;
        VertexAttribManager attribs = new VertexAttribManager();
        private void T18_ParticlesGameWindow_Load(object sender, EventArgs e) {

            VSync = VSyncMode.Off;
            GL.Enable(EnableCap.DepthTest);
            GL.DepthFunc(DepthFunction.Less);

            GL.ClearColor(0, 0, .4f, 0);


            vao = new Vao();
            shader = new Shader("shader.vs", "shader.fs");
            

            for (int i = 0; i < MaxParticles; i++) {
                var particle = new Particle();
                particle.life = -1.0f;
                particle.cameradistance = -1.0f;
                ParticlesContainer.Add(particle);
            }

            texture = Texture2D.Load("particle.dds");
            billboardVertexBuffer = new Vbo<float>(g_vertex_buffer_data);
            particlesPositionBuffer = new Vbo<float>(BufferTarget.ArrayBuffer, BufferUsageHint.StreamDraw, MaxParticles*4*sizeof(float));
            particlesColorBuffer = new Vbo<float>(BufferTarget.ArrayBuffer, BufferUsageHint.StreamDraw, MaxParticles * 4 * sizeof(byte));
            attribs.AddAttrib(3, billboardVertexBuffer);
            attribs.AddAttrib(4, particlesPositionBuffer);
            attribs.AddAttrib(4, particlesColorBuffer, VertexAttribPointerType.UnsignedByte, true);

        }
        Random random = new Random();
        int rand() {
            return random.Next();
        }
        float glmLength2(Vector3 a) {
            return Vector3.Dot(a, a);
        }
        private  void Render() {
            //InputControls.LockCursor = false;
            InputControls.computeMatricesFromInput(this);
            double delta = InputControls.getDeltaTime();

            var projection = InputControls.ProjectionMatrix;
            var view = InputControls.ViewMatrix;
            Vector3 CameraPosition = view.Inverted().Row3.Xyz;
            var viewProjection = view * projection;
            GenerateParticles(delta, CameraPosition);
            particlesPositionBuffer.Bind();
            GL.BufferData(particlesPositionBuffer.BufferTarget, MaxParticles * 4 * sizeof(float), default, particlesPositionBuffer.UsageHint);
            GL.BufferSubData<float>(BufferTarget.ArrayBuffer, IntPtr.Zero, this.ParticlesCount * sizeof(float) * 4, g_particule_position_size_data);

            particlesColorBuffer.Bind();
            GL.BufferData(particlesColorBuffer.BufferTarget, MaxParticles * 4 * sizeof(byte), default, BufferUsageHint.StreamDraw);
            GL.BufferSubData<byte>(BufferTarget.ArrayBuffer, default, ParticlesCount * sizeof(byte) * 4, g_particule_color_data);

            GL.Enable(EnableCap.Blend);
            GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);

            shader.Use();
            texture.Active(TextureUnit.Texture0, shader, "myTextureSampler");
            shader.SetVector3("CameraRight_worldspace",view.Column0.Xyz);
            shader.SetVector3("CameraUp_worldspace", view.Column1.Xyz);
            shader.SetMatrix4("VP", ref viewProjection);
            attribs.Enable();
            GL.VertexAttribDivisor(0, 0);
            GL.VertexAttribDivisor(1, 1);
            GL.VertexAttribDivisor(2, 1);
            GL.DrawArraysInstanced(PrimitiveType.TriangleStrip, 0, 4, ParticlesCount);
            attribs.Disable();



        }
        int ParticlesCount;
        private void GenerateParticles(double delta, Vector3 CameraPosition) {
            int newparticles = (int)(delta * 10000.0);
            if (newparticles > (int)(0.016f * 10000.0))
                newparticles = (int)(0.016f * 10000.0);

            for (int i = 0; i < newparticles; i++) {
                int particleIndex = FindUnusedParticle();
                var currentParticle = ParticlesContainer[particleIndex];
                currentParticle.life = 5.0f; // This particle will live 5 seconds.
                currentParticle.pos = new Vector3(0, 0, -20.0f);

                float spread = 1.5f;
                Vector3 maindir = new Vector3(0.0f, 10.0f, 0.0f);
                // Very bad way to generate a random direction; 
                // See for instance http://stackoverflow.com/questions/5408276/python-uniform-spherical-distribution instead,
                // combined with some user-controlled parameters (main direction, spread, etc)
                Vector3 randomdir = new Vector3(
                    (rand() % 2000 - 1000.0f) / 1000.0f,
                    (rand() % 2000 - 1000.0f) / 1000.0f,
                    (rand() % 2000 - 1000.0f) / 1000.0f
                );

                currentParticle.speed = maindir + randomdir * spread;


                // Very bad way to generate a random color
                currentParticle.r = (byte)(rand() % 256);
                currentParticle.g = (byte)(rand() % 256);
                currentParticle.b = (byte)(rand() % 256);
                currentParticle.a = (byte)((rand() % 256) / 3);

                currentParticle.size = (rand() % 1000) / 2000.0f + 0.1f;
                ParticlesContainer[particleIndex] = currentParticle;

            }



            // Simulate all particles
             ParticlesCount = 0;
            for (int i = 0; i < MaxParticles; i++) {

                //Particle & p = ParticlesContainer[i]; // shortcut
                Particle p = ParticlesContainer[i];

                if (p.life > 0.0f) {

                    // Decrease life
                    p.life -= (float)delta;
                    if (p.life > 0.0f) {

                        // Simulate simple physics : gravity only, no collisions
                        p.speed += new Vector3(0.0f, -9.81f, 0.0f) * (float)delta * 0.5f;
                        p.pos += p.speed * (float)delta;
                        p.cameradistance = glmLength2(p.pos - CameraPosition);
                        //ParticlesContainer[i].pos += glm::vec3(0.0f,10.0f, 0.0f) * (float)delta;

                        // Fill the GPU buffer
                        g_particule_position_size_data[4 * ParticlesCount + 0] = p.pos.X;
                        g_particule_position_size_data[4 * ParticlesCount + 1] = p.pos.Y;
                        g_particule_position_size_data[4 * ParticlesCount + 2] = p.pos.Z;

                        g_particule_position_size_data[4 * ParticlesCount + 3] = p.size;

                        g_particule_color_data[4 * ParticlesCount + 0] = p.r;
                        g_particule_color_data[4 * ParticlesCount + 1] = p.g;
                        g_particule_color_data[4 * ParticlesCount + 2] = p.b;
                        g_particule_color_data[4 * ParticlesCount + 3] = p.a;

                    }
                    else {
                        // Particles that just died will be put at the end of the buffer in SortParticles();
                        p.cameradistance = -1.0f;
                    }

                    ParticlesCount++;

                }
                ParticlesContainer[i] = p;
            }

            SortParticles();
        }

        private void T18_ParticlesGameWindow_Resize(object sender, EventArgs e) {
            GL.Viewport(0, 0, Width, Height);
        }



        private void T18_ParticlesGameWindow_Unload(object sender, EventArgs e) {
            GlDisposableObjectBase.DisposeObjects(vao, shader,texture,billboardVertexBuffer,particlesPositionBuffer,particlesColorBuffer);
        }

    }
}
